folditfandomcom-20200222-history
Using pcall to trap Lua errors
The V2 Foldit Lua interface tends to skip issuing return codes. For example, if you attempt to close a cut point with structure.DeleteCut(), there's no indication of whether the operation succeeded. (Maybe there was no cut point there, maybe the cut point failed to close because the ends were too far apart, you'll never know). Worse, some simple errors will terminate a recipe. One frequent offender is the function rotamer.SetRotamer(). For a given segment, this function lets you "snap" the sidechain to a numbered position. Unfortunately, the number of rotamer positions available can change based on the overall pose of the protein. But rotamer.SetRotamer() just throws an error if you specify a currently invalid rotamer position. So unless recipes are hyper-vigilant about calling rotamer.GetCount() beforehand, rotamer.SetRotamer() tends to cause a crash. Fortunately, the Lua function pcall(), offers a "protected call" to a function that won't crash a recipe. Using the pcall() function, a problematic routine like SetRotamer can be reformed to produce an error code. The pcall() function and the related xpcall() function are discussed in detail in Lua Error Handling. The first step is to create a wrapper function around the problematic function. Here the function SafeSnap() can serve as a plug-compatible replacment for rotamer.SetRotamer. -- -- SafeSnap uses the Lua protected call function, pcall, -- to call rotamer.SetRotamer -- -- SafeSnap returns a numeric return code and an error message. -- -- The return codes are: -- -- 0 - successful, error message is nil -- -1 - bad rotamer index -- -- Errors other than a bad rotamer index are rethrown using -- the Lua function error(). -- function SafeSnap ( segIdx, rotIdx ) -- -- error message when attempting to snap to an invalid rotamer -- local BADSNAP = "(snap index out of bounds)" -- -- don't allow a bad rotamer to stop us -- local good, errmsg = pcall ( rotamer.SetRotamer, segIdx, rotIdx ) if good then return 0, nil else local err2 = ParseError ( errmsg ) local errp = err2:find ( BADSNAP ) if errp ~= nil then return -1, err2 end error ( errmsg, 0 ) end end One side effect of using pcall() is that any error messages received won't have the function in them. So instead of "SetRotamer", you'd see just a "?". For this reason, SafeSnap() looks for just the "(snap index out of bounds)" part of the error message. If an error is returned for some other reason, SafeSnap() simply "rethrows" the error using the Lua error() function. The original error message is used to rethrow the error, along with the "0" to indicate that Lua should not add any context information to the error message. The rethrown error is then caught either by the default error handler, or a custom error handler specified on the xpcall() routine. (You really, really, really should use xpcall() in all but the most trivial recipes.) The SafeSnap() function uses the helper function ParseError(), which is based on the cleanup logic developed by Jean-Bob. Similar logic is found in the model cleanup function on the Model Recipes page. function ParseError ( errmsg ) local reason local start, stop, line, msg start, stop, line, msg = errmsg:find ( ":(%d+):%s()" ) if msg ~= nil then errmsg = errmsg:sub ( msg, #errmsg ) end return errmsg end The final step is to use SafeSnap() in place of rotamer.SetRotamer(): -- -- attempt to snap the sidechain in a safe, non-crashy manner -- local rc, errmsg = SafeSnap ( segIdx, rotIdx ) if rc ~= 0 then print ( "optimize: BAD SNAP, segment = " .. segIdx .. ", rotamer = " .. rotIdx .. ", current rotamer count = " .. rotamer.GetCount ( segIdx ) ) In this example, if a bad snap occurs, the recipe simply logs the fact and keeps on going. The nice thing about having a return code is that the calling function has all the necessary information, and can log a detailed message for debugging purposes. The approach outlined here can be applied to any other Foldit function that lacks a useful return code. Category:Script tutorial